<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Document</title>
    <!-- <script src="./reactivity.js"></script> -->
  </head>
  <body>
    <div id="app"></div>
    <script>
      function shouldSetAsProps(el, key, value) {
        if (key === 'form' && el.tagName === 'INPUT') return false
        return key in el
      }
      const Text = Symbol() // 文本节点
      const Comment = Symbol() // 注释节点
      const Fragment = Symbol() // Fragment
      function createRender(options) {
        const {
          createElement,
          insert,
          setElementText,
          patchProps,
          createText,
          setText,
          createComment,
        } = options
        // 卸载函数
        function unmount(vnode) {
          if (vnode.type === Fragment) {
            vnode.children.forEach((c) => unmount(c))
            return
          }
          const parent = vnode.el.parentNode
          if (parent) {
            parent.removeChild(vnode.el)
          }
        }
        // 挂载
        function mountElement(vnode, container, anchor) {
          // 以vnode.type的值作为标签名创建新的DOM元素
          const el = (vnode.el = createElement(vnode.type))
          // 处理子节点，如果说子节点是字符串，那么代表元素具有文本节点
          if (typeof vnode.children === 'string') {
            // 这时只需要设置元素的textContent即可
            setElementText(el, vnode.children)
          } else if (Array.isArray(vnode.children)) {
            // 遍历每一个子节点，并调用patch函数挂载它们
            vnode.children.forEach((child) => {
              patch(null, child, el)
            })
          }

          if (vnode.props) {
            for (const key in vnode.props) {
              patchProps(el, key, null, vnode.props[key])
            }
          }

          // 将创建好的DOM元素添加到容器中去
          insert(el, container, anchor)
          // 这样就完成了一个vnode的挂载
        }

        // 更新DOM元素操作
        function patchElement(n1, n2) {
          const el = (n2.el = n1.el)
          const oldProps = n1.props
          const newProps = n2.props
          // 第一步更新props
          for (const key in newProps) {
            if (
              oldProps &&
              oldProps.hasOwnProperty(key) &&
              newProps[key] !== oldProps[key]
            ) {
              patchProps(el, key, oldProps[key], newProps[key])
            }
          }

          for (const key in oldProps) {
            if (!(key in newProps)) {
              patchProps(el, key, oldProps[key], null)
            }
          }

          // 第二步，更新children
          patchChildren(n1, n2, el)
        }

        // 更新子节点
        function patchChildren(n1, n2, container) {
          // 判断新子节点的类型是否是文本节点
          if (typeof n2.children === 'string') {
            // 旧子节点的类型有三种可能：没有子节点、文本子节点和一组子节点
            // 只有当旧的子节点为一组子节点时，才需要逐个卸载，其他情况下什么都不需要做
            if (Array.isArray(n1.children)) {
              n1.children.forEach((c) => unmount(c))
            }
            setElementText(container, n2.children)
          } else if (Array.isArray(n2.children)) {
            if (Array.isArray(n1.children)) {
              // 代码运行到这里的话，说明新旧子节点都是一组子节点，这里涉及核心的diff算法
              // 封装patchKeyedChildren函数处理两组子节点
              patchKeyedChildren(n1, n2, container)
            } else {
              // 此时旧子节点要么为空，要么就是文本子节点
              // 不管哪种情况，都应该先将容器清空
              setElementText(container, '')
              // 然后将新的一组子节点逐个挂载
              n2.children.forEach((c) => patch(null, c, container))
            }
          } else {
            // 代码运行到这里的时候，说明新子节点不存在
            // 旧子节点是一组子节点的话，只需要逐个卸载即可
            if (Array.isArray(n1.children)) {
              n1.children.forEach((c) => unmount(c))
            } else if (typeof n1.children === 'string') {
              // 旧子节点是文本节点的话，直接清空内容即可
              setElementText(container, '')
            } else {
              // 如果也没有旧子节点，则什么都不用做
            }
          }
        }

        // 我们将两组子节点打补丁的工作封装到这里
        function patchKeyedChildren(n1, n2, container) {
          // 获取旧子节点
          const oldChildren = n1.children
          // 获取新子节点
          const newChildren = n2.children
          // 创建四个索引值
          let oldStartIdx = 0 // 旧的一组子节点的第一个子节点
          let oldEndIdx = oldChildren.length - 1 // 旧的一组子节点的最后一个子节点
          let newStartIdx = 0 // 新的一组子节点的第一个子节点
          let newEndIdx = newChildren.length - 1 // 新的一组子节点的最后一个子节点
          // 四个索引指向的vnode节点
          let oldStartVNode = oldChildren[oldStartIdx]
          let oldEndVNode = oldChildren[oldEndIdx]
          let newStartVNode = newChildren[newStartIdx]
          let newEndVNode = newChildren[newEndIdx]
          while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {
            if (!oldStartVNode) {
              oldStartVNode = oldChildren[++oldStartIdx]
            } else if (!oldEndVNode) {
              oldEndVNode = oldChildren[--oldEndIdx]
            } else if (oldStartVNode.key === newStartVNode.key) {
              // 第一步 旧的一组子节点中的第一个子节点和新的一组子节点中的第一个子节点比较
              patch(oldStartVNode, newStartVNode, container)
              oldStartVNode = oldChildren[++oldStartIdx]
              newStartVNode = newChildren[++newStartIdx]
            } else if (oldEndVNode.key === newEndVNode.key) {
              // 第二步 旧的一组子节点中的最后一个子节点和新的一组子节点中的最后一个子节点比较
              patch(oldEndVNode, newEndVNode, container)
              oldEndVNode = oldChildren[--oldEndIdx]
              newEndVNode = newChildren[--newEndIdx]
            } else if (oldStartVNode.key === newEndVNode.key) {
              // 第三步 旧的一组子节点中的第一个子节点和新的一组子节点中的最后一个子节点比较
              patch(oldStartVNode, newEndVNode, container)
              insert(oldStartVNode.el, container, oldEndVNode.el.nextSibling)
              oldStartVNode = oldChildren[++oldStartIdx]
              newEndVNode = newChildren[--newEndIdx]
            } else if (oldEndVNode.key === newStartVNode.key) {
              // 第四步 旧的一组子节点中的最后一个子节点和新的一组子节点中的第一个子节点比较
              // 发现可以复用，调用patch函数打补丁操作
              patch(oldEndVNode, newStartVNode, container)
              // 移动DOM
              insert(oldEndVNode.el, container, oldStartVNode.el)
              // 移动结束以后更新索引值，并指向下一个位置
              oldEndVNode = oldChildren[--oldEndIdx]
              newStartVNode = newChildren[++newStartIdx]
            } else {
              // 遍历一组旧的子节点，试图寻找与newStartVNode拥有相同key值的节点
              // idxInOld 就是新的一组子节点的头部节点在旧的一组子节点中的索引
              const idxInOld = oldChildren.findIndex(
                (node) => node.key === newStartVNode.key
              )
              // idxInOld大于0说明找到了可复用的节点，并且需要将其对应的真实DOM移动到头部
              if (idxInOld > 0) {
                // idxInOld位置对应的就是要移动的节点，存储在vnodeToMove变量中
                const vnodeToMove = oldChildren[idxInOld]
                // 打补丁
                patch(vnodeToMove, newStartVNode, container)
                // 将vnodeToMove移动到头部节点oldStartVNode.el的位置之前，因此使用后者作为锚点
                insert(vnodeToMove.el, container, oldStartVNode.el)
                // idxInOld的节点已经移动到了别处，所以将其设置为undefined
                oldChildren[idxInOld] = undefined
              } else {
                // 将newStartVNode作为新节点挂载到头部，使用当前头部节点oldStartVNode.el作为锚点
                patch(null, newStartVNode, container, oldStartVNode.el)
              }
              // 更新newStartIdx到下一个位置
              newStartVNode = newChildren[++newStartIdx]
            }
          }
          // 循环结束后检查索引值的情况
          if (oldEndIdx < oldStartIdx && newStartIdx <= newEndIdx) {
            // 如果满足条件，则说明有新节点被遗漏，需要挂载它们
            for (let i = newStartIdx; i <= newEndIdx; i++) {
              const anchor = newChildren[newEndIdx + 1]
                ? newChildren[newEndIdx + 1].el
                : null
              // 锚点仍然使用的是oldStartVNode.el
              patch(null, newChildren[i], container, anchor)
            }
          } else if (newEndIdx < newStartIdx && oldStartIdx <= oldEndIdx) {
            // 移除操作
            for (let i = oldStartIdx; i <= oldEndIdx; i++) {
                unmount(oldChildren[i])
            }
          }
        }

        function patch(n1, n2, container, anchor) {
          // 如果两个vnode的元素类型不同，直接卸载旧的，然后再向下执行，挂载新的
          if (n1 && n1.type !== n2.type) {
            unmount(n1)
            n1 = null
          }
          const { type } = n2
          if (typeof type === 'string') {
            // 如果说旧节点不存在，那么就意味着是挂载，调用mountElement方法
            if (!n1) {
              mountElement(n2, container, anchor)
            } else {
              // 如果有旧节点，那么意味着打补丁
              patchElement(n1, n2)
            }
          } else if (typeof type === 'object') {
            // n2.type的值类型是对象，那么说明是一个组件
          } else if (type === Text) {
            // 处理其他类型的vnode
            if (!n1) {
              // 使用createTextNode创建文本节点
              const el = (n2.el = createText(n2.children))
              // 将文本节点插入到容器中
              insert(el, container)
            } else {
              const el = (n2.el = n1.el)
              if (n2.children !== n1.children) {
                setText(el, n2.children)
              }
            }
          } else if (type === Comment) {
            if (!n1) {
              // 使用createTextNode创建文本节点
              const el = (n2.el = createComment(n2.children))
              // 将文本节点插入到容器中
              insert(el, container)
            } else {
              const el = (n2.el = n1.el)
              if (n2.children !== n1.children) {
                setText(el, n2.children)
              }
            }
          } else if (type === Fragment) {
            if (!n1) {
              n2.children.forEach((c) => patch(null, c, container))
            } else {
              patchChildren(n1, n2, container)
            }
          }
        }
        function render(vnode, container) {
          if (vnode) {
            // 新vnode存在，并且旧的vnode一起传递给patch函数，进行打补丁
            patch(container._vnode, vnode, container)
          } else {
            if (container._vnode) {
              // 旧vnode存在，且新vnode不存在，说明是卸载
              // 只需要将container内的dom清空即可
              // 拿到真实DOM
              // 拿到真实DOM的父元素
              // 从父元素中移除
              unmount(container._vnode)
            }
          }
          // 把vnode存储到container的_vnode瞎，也就是后续渲染中的旧vnode
          container._vnode = vnode
        }
        return {
          render,
        }
      }

      const render2 = createRender({
        createElement(tag) {
          return document.createElement(tag)
        },
        setElementText(el, text) {
          el.textContent = text
        },
        createComment(text) {
          return document.createComment(text)
        },
        setComment(el, text) {
          el.nodeValue = text
        },
        createText(text) {
          return document.createTextNode(text)
        },
        setText(el, text) {
          el.nodeValue = text
        },
        insert(el, parent, anchor = null) {
          parent.insertBefore(el, anchor)
        },
        patchProps(el, key, prevValue, nextValue) {
          if (/^on/.test(key)) {
            // 定义el._vei为一个对象，存在事件名称到事件处理函数的映射
            const invokers = el._vei || (el._vei = {})
            let invoker = invokers[key]
            // 根据属性名称得到对应的事件名称 例如 onClick -> click
            const name = key.slice(2).toLowerCase()
            if (nextValue) {
              if (!invoker) {
                // 如果没有invoker，则伪造一个invoker缓存到el._vei中
                // vei是 vue event invoker的首字母缩写
                // 将事件处理函数缓存到el._vei[key]下，避免覆盖
                invoker = el._vei[key] = (e) => {
                  // 如果事件发生的时间早于事件处理函数绑定的时间，则不执行事件处理函数
                  if (e.timeStamp < invoker.attached)
                    if (Array.isArray(invoker.value)) {
                      // 如果invoker.value是一个数组，那么遍历它并逐个调用事件处理函数
                      invoker.value.forEach((fn) => fn(e))
                    } else {
                      invoker.value(e)
                    }
                }
                // 将真正的事件处理函数nextValue赋值给invoker.value
                invoker.value = nextValue
                // 添加invoker.attached属性，存储事件处理函数被绑定的事件
                invoker.attached = performance.now()
                // 绑定invoker作为事件处理函数
                el.addEventListener(name, invoker)
              } else {
                // 如果invoker存在，意味着更新，并且只需要更新invoker.value的值
                invoker.value = nextValue
              }
            } else if (invoker) {
              // 新的事件绑定函数不存在，且之前绑定的invoker存在，则移除上一次的事件回调函数
              el.removeEventListener(name, invoker)
            }
          } else if (key === 'class') {
            el.className = nextValue || ''
          } else if (shouldSetAsProps(el, key, nextValue)) {
            const type = typeof el[key]
            if (type === 'boolean' && nextValue === '') {
              el[key] = true
            } else {
              el[key] = nextValue
            }
          } else {
            el.setAttribute(key, nextValue)
          }
        },
      })
      const oldVNode = {
        type: 'div',
        children: [
          { type: 'p', children: '1', key: 1 },
          { type: 'p', children: '2', key: 2 },
          { type: 'p', children: '3', key: 3 },
        ],
      }

      const newVNode = {
        type: 'div',
        children: [
          { type: 'p', children: '1', key: 1 },
          { type: 'p', children: '2', key: 2 },
        ],
      }
      render2.render(oldVNode, document.querySelector('#app'))
      setTimeout(() => {
        console.log('update')
        render2.render(newVNode, document.querySelector('#app'))
      }, 1000)
    </script>
  </body>
</html>
